/**
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to marcus@lateralminds.com.au so I can send you a copy immediately.
 *
 * @copyright  Copyright (c) 2008 Lateral Minds (http://www.lateralminds.com.au)
 * @version    $Id$
 * @license    New BSD License
 */
package com.lateralminds.alfresco.util;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.importer.ImporterBootstrap;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.MLText;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.version.Version;
import org.alfresco.service.cmr.version.VersionHistory;
import org.alfresco.service.cmr.version.VersionService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.apache.log4j.Logger;

import de.schlichtherle.io.FileInputStream;

/**
 * @author Carsten Eckelmann
 *
 */
public class LateralServiceImpl implements LateralService
{

    /**
     * The repository store ref
     */
    private StoreRef storeRef;

    /**
     * The company home space
     */
    private NodeRef companyHome;
    
    /**
     * Get the storeRef being used
     * 
     * @return
     */
    private StoreRef getStoreRef()
    {
        if (storeRef == null) {
            storeRef = bootstrap.getStoreRef();
        }

        return storeRef;
    }
    
    /*
     * (non-Javadoc)
     * @see com.lateralminds.alfresco.util.LateralService#getCompanyHome()
     */
    public NodeRef getCompanyHome()
    {
        if (companyHome == null) {
            List<NodeRef> results = this.searchForNodeRefs("PATH:\"/app:company_home\"");
            if (results.size() > 0) {
                companyHome = results.get(0);
            }
        }
        
        return companyHome;
    }

    /**
     * Get the working copy for a given document
     * 
     * @param nodeRef
     * @return
     */
    public NodeRef getWorkingCopy(NodeRef nodeRef)
    {
        NodeRef workingCopy = null;

        // Do a search to find the working copy document
        ResultSet resultSet = null;

        
        try {
            // query = "+ASPECT:\""+ContentModel.ASPECT_WORKING_COPY+"\"+@cm\\:source:\""+node+"\"";
            resultSet = this.searchService.query(nodeRef.getStoreRef(), SearchService.LANGUAGE_LUCENE, "+ASPECT:\""
                    + ContentModel.ASPECT_WORKING_COPY.toString() + "\" +@\\{http\\://www.alfresco.org/model/content/1.0\\}"
                    + ContentModel.PROP_COPY_REFERENCE.getLocalName() + ":\"" + nodeRef.toString() + "\"");
            if (resultSet.getNodeRefs().size() != 0) {
                workingCopy = resultSet.getNodeRef(0);
            }
        }
        finally {
            if (resultSet != null) {
                resultSet.close();
            }
        }

        return workingCopy;
    }

    /**
     * Get a result set for a given lucene search term.
     * 
     * @param searchString
     * @param searchSvc
     * @return
     */
    public ResultSet search(String searchString)
    {
        return search(searchString, null, false);
    }

    public ResultSet search(String searchString, String sortBy, Boolean asc)
    {
        SearchParameters sp = new SearchParameters();
        sp.addStore(getStoreRef());
        sp.setLanguage(SearchService.LANGUAGE_LUCENE);

        // TODO: Change this back once the alfresco lucene sorting bug is fixed

        /*
         * if (sortBy != null) { if (asc == null) asc = true; sp.addSort(sortBy,
         * asc); }
         */

        sp.setQuery(searchString);
        ResultSet results = this.searchService.query(sp);
        return results;
    }

    /**
     * Perform a lucene search and get back a list of noderefs
     * 
     * @param searchString
     * @return
     */
    public List<NodeRef> searchForNodeRefs(String searchString)
    {
        return this.searchForNodeRefs(searchString, null, null);
    }

    /**
     * Perform a search and get back a list of noderefs.
     * 
     * @param searchString
     * @param sortBy
     * @param asc
     * @return
     */
    public List<NodeRef> searchForNodeRefs(String searchString, String sortBy, Boolean asc)
    {
        SearchParameters sp = new SearchParameters();
        sp.addStore(getStoreRef());
        sp.setLanguage(SearchService.LANGUAGE_LUCENE);

        // TODO: Change this back once the alfresco lucene sorting bug is fixed
        /*
         * if (sortBy != null) { if (asc == null) asc = true; sp.addSort(sortBy,
         * asc); }
         */

        if (logger.isDebugEnabled()) {
            logger.debug("Searching for '" + searchString + "'");
        }

        sp.setQuery(searchString);
        ResultSet results = searchService.query(sp);

        List<NodeRef> refs = new ArrayList<NodeRef>();
        for (NodeRef ref : results.getNodeRefs()) {
            if (!refs.contains(ref)) {
                refs.add(ref);
            }
        }
        if (sortBy != null) {
            ResultSetSorter sorter = new ResultSetSorter(refs, sortBy.replace("@", ""), asc, nodeService, namespaceService);
            sorter.sort();
        }

        results.close();

        return refs;
    }
    
    /**
     * Set the content of a given node. 
     * 
     * @param node
     * @param content
     * @param mimeType
     * @param property
     */
    public void setNodeContent(NodeRef node, String content, String mimeType, QName property)
    {
        ContentWriter writer;
        writer = this.contentService.getWriter(node, property, true);
        writer.setMimetype(mimeType);
        writer.putContent(content);
        writer.setEncoding("UTF-8");
    }
    
    /**
     * Set the content of a given node. 
     * 
     * @param node
     * @param content
     * @param mimeType
     * @param property
     */
    public void setNodeContent(NodeRef node, InputStream content, String mimeType, QName property)
    {
        ContentWriter writer;
        writer = this.contentService.getWriter(node, property, true);
        writer.setMimetype(mimeType);
        writer.putContent(content);
        writer.setEncoding("UTF-8");
    }

    /**
     * Set the content for a given node.
     * 
     * @param node
     * @param content
     * @param svc
     */
    public void setNodeContent(NodeRef node, String content, String mimeType)
    {
        this.setNodeContent(node, content, mimeType, ContentModel.PROP_CONTENT);
    }

    /**
     * Set the content for a given node.
     * 
     * @param node
     * @param content
     * @param svc
     */
    public void setNodeContent(NodeRef node, InputStream content, String mimeType)
    {
        this.setNodeContent(node, content, mimeType, ContentModel.PROP_CONTENT);
    }

    /**
     * Get the content for a given noderef.
     * 
     * @param nodeRef
     * @return
     */
    public String getNodeContent(NodeRef nodeRef)
    {
        return this.getNodeContent(nodeRef, ContentModel.PROP_CONTENT);
    }
    
    /**
     * Get the content for a given noderef's property
     * @param nodeRef
     * @param property
     * @return
     */
    public String getNodeContent(NodeRef nodeRef, QName property)
    {
        ContentReader reader = this.contentService.getReader(nodeRef, property);
        return reader.getContentString();
    }

    /**
     * Get the extension of the given string
     * 
     * @param name The name of the file to get the extension for
     * @return
     */
    public String getExtension(String name)
    {
        int extIndex = name.lastIndexOf('.');
        if (extIndex != -1) {
            String ext = name.substring(extIndex + 1).toLowerCase();
            return ext;
        }
        
        return null;
    }

    /**
     * Get the mimetype for a given filename
     * 
     * @param filename
     * @param mimetypeService
     */
    public String getMimetypeFor(String filename)
    {
        // fall back to binary mimetype if no match found
        String mimetype = MimetypeMap.MIMETYPE_BINARY;
        int extIndex = filename.lastIndexOf('.');
        if (extIndex != -1) {
            String ext = filename.substring(extIndex + 1).toLowerCase();
            String mt = mimetypeService.getMimetypesByExtension().get(ext);
            if (mt != null) {
                mimetype = mt;
            }
        }

        return mimetype;
    }
    
    private List<String> imageExts = Arrays.asList(new String[] {"jpg", "bmp", "gif", "png", "jpeg", "tiff"}); 
    
    /**
     * Indicates whether the given noderef is an image or not
     * 
     * @param ref
     * @return
     */
    public boolean isImage(NodeRef ref)
    {
        // get the mimetype
        String ext = this.getExtension(this.refName(ref));
        if (imageExts.contains(ext)) {
            return true;
        }
        return false;
    }

    /**
     * Get the name for a given noderef.
     * 
     * @param n
     * @param ns
     * @return
     */
    public String refName(NodeRef n)
    {
        return (String) nodeService.getProperty(n, ContentModel.PROP_NAME);
    }

    /**
     * Get the title for a noderef, returning the name if the title is null
     * 
     * @param n
     * @param ns
     * @return
     */
    public String refTitle(NodeRef n)
    {

        String title = nodeService.getProperty(n, ContentModel.PROP_TITLE) instanceof MLText ? ((MLText) nodeService.getProperty(n,
                ContentModel.PROP_TITLE)).getDefaultValue() : (String) nodeService.getProperty(n, ContentModel.PROP_TITLE);
        if (title == null || title.length() == 0) {
            return refName(n);
        }
        return title;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.lateralminds.alfresco.util.LateralService#emptyDirectory(java.io.File)
     */
    public boolean emptyDirectory(File path)
    {
        if (path.exists()) {
            File[] files = path.listFiles();
            for (int i = 0; i < files.length; i++) {
                if (files[i].isDirectory()) {
                    if (!emptyDirectory(files[i])) {
                        return false;
                    }
                }
                files[i].delete();
            }
        }
        return true;
    }

    /**
     * Cleans the uploaded filename to just the document name (as IE sends the
     * full path name...)
     * 
     * @param uploadName
     * @return
     */
    public String cleanFileUploadName(String uploadName)
    {

        if (uploadName.indexOf('/') >= 0) {
            uploadName = uploadName.substring(uploadName.lastIndexOf('/') + 1);
        }

        if (uploadName.indexOf('\\') >= 0) {
            uploadName = uploadName.substring(uploadName.lastIndexOf('\\') + 1);
        }

        return uploadName;
    }

    /**
     * Utility method to create a node with the parameters specified. Checks for
     * existing child node of the parent with the same name as that specified.
     * If it exists, this method will throw an {@link IllegalArgumentException}.
     * 
     * @param parent
     *            The parent of the node to be created.
     * @param name
     *            The cm:name of the node to be created.
     * @param assocType
     *            The association type between the node and the parent.
     * @param type
     *            The type of the node.
     * @param content
     *            The content to store in the node.
     * @param mimeType
     *            The mimetype of the content.
     * @param props
     *            The properties to assign.
     * @return {@link NodeRef} of the node created.
     * @throws IllegalArgumentException
     *             if the parent already has a child node with the name
     *             specified.
     */
    public NodeRef createNode(NodeRef parent, String name, QName assocType, QName type, byte[] content, String mimeType,
            Map<QName, Serializable> props) throws IllegalArgumentException
    {
        NodeRef existingNode = null;
        
        //TODO: this could be refactored using fileFolderService.searchSimple()
        
        List<ChildAssociationRef> childs = nodeService.getChildAssocs(parent);
        for (ChildAssociationRef child : childs) {
            String childName = (String) nodeService.getProperty(child.getChildRef(), ContentModel.PROP_NAME);
            if (childName.equals(name) && assocType.equals(child.getTypeQName())) {
                existingNode = child.getChildRef();
                break;
            }
        }
        if (existingNode != null) {
            throw new IllegalArgumentException("Child node of name " + name + " and association type: " + assocType.toString()
                    + " already exists for parent " + parent.getId().toString());
        }

        if (props == null) {
            props = new HashMap<QName, Serializable>();
        }

        props.put(ContentModel.PROP_NAME, name);
        NodeRef createdNode = nodeService.createNode(parent, assocType, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name),
                type, props).getChildRef();
        
        // only set the content if content is given
        if (content != null && content.length > 0) {
            ContentWriter writer = contentService.getWriter(createdNode, ContentModel.PROP_CONTENT, true);
    
            // default mime type if mimeType is null
            writer.setMimetype(mimeType != null ? mimeType : getMimetypeFor(name));
            
            writer.putContent(new ByteArrayInputStream(content));
        }
        return createdNode;
    }
    
    /*
     * (non-Javadoc)
     * @see com.lateralminds.alfresco.util.LateralService#retrieveChildNode(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, org.alfresco.service.namespace.QName)
     */
    public NodeRef retrieveChildNode(NodeRef parent, String name, QName type)
    {
        NodeRef existing = nodeService.getChildByName(parent, ContentModel.ASSOC_CONTAINS, name);
        if (existing != null) return existing;

        existing = this.fileFolderService.create(parent, name, type).getNodeRef();

        return existing;
    }

    

    /*
     * (non-Javadoc)
     * 
     * @see com.lateralminds.alfresco.util.LateralService#getAssociatedNodeRefsAtVersion(org.alfresco.service.cmr.repository.NodeRef,
     *      java.lang.String, java.util.List)
     */
    public List<NodeRef> getAssociatedNodeRefsAtVersion(NodeRef ref, String versionLabel, List<AssociationRef> associations)
    {
        List<NodeRef> versionNodeRefs = new ArrayList<NodeRef>(associations.size());
        
        VersionHistory history = versionService.getVersionHistory(ref);
        // Node probably didn't have versionable applied, so just return the associated
        // ones. 
        if (history == null /* TODO: Should we bother checking? */ || versionLabel == null || versionLabel.length() == 0) {
            for (AssociationRef assocRef : associations) {
                versionNodeRefs.add(assocRef.getTargetRef());
            }
        } else {
            Version version = history.getVersion(versionLabel);
            
            Date maxDate = version.getCreatedDate();
    
            // For each association, find a version of the noderef that is closest to 
            // the date of the version we're looking for. 
            for (AssociationRef assocRef : associations) {
                NodeRef versionRef = this.getNearestVersionNodeRef(assocRef.getTargetRef(), maxDate);
                if (versionRef != null) {
                    versionNodeRefs.add(versionRef);
                }
            }
        }

        return versionNodeRefs;
    }

    /**
     * Get the nearest version noderef of targetRef to the maxDate
     * 
     * @param targetRef the noderef whose versions we are examining
     * @param maxDate the maximum date that the version can be 
     * @return The noderef who's version date is closest but not over maxdate
     */
    private NodeRef getNearestVersionNodeRef(NodeRef targetRef, Date maxDate)
    {

        // If there was no maxDate to compare against, then we can just add the
        // noderef
        // to the result set immediately
        if (maxDate == null) {
            return targetRef;
        }

        VersionHistory targetHistory = versionService.getVersionHistory(targetRef);

        // If it's not versioned, just add it straight up
        if (targetHistory == null || targetHistory.getAllVersions().size() == 0) {
            return targetRef;
        }
        
        Version targetVersion = null;
        
        // We're going to go through all of the target noderef's versions 
        // to find one whose created date is closest but not greater than the 
        // maxDate
        Iterator<Version> iter = targetHistory.getAllVersions().iterator();

        // We'll keep track of which ref we're after here.
        // basically, we keep iterating the versions until we get to one that's
        // version create date is less than, but closest to the maxDate above
        Version closestVersion = null;
        
        // This has been deliberately laid out over several explicit checks to
        // try and make the code easier to follow.
        while (iter.hasNext()) {
            targetVersion = iter.next();
            Date versionDate = targetVersion.getCreatedDate();

            // If the version is newer than the noderef we're examining, keep
            // going
            if (versionDate.after(maxDate)) {
                continue;
            }

            // If we haven't got a closest yet, set one
            if (closestVersion == null) {
                closestVersion = targetVersion;
                continue;
            }

            // See if the version date is closer to the maxdate
            // than the current closestDate
            if (versionDate.after(closestVersion.getCreatedDate())) {
                closestVersion = targetVersion;
            }
        }

        if (closestVersion != null) {
            return closestVersion.getFrozenStateNodeRef();
        }

        return null;
    }
    
    public  NodeRef getRefByPath(String path) {
        NodeRef ref = null;

        String query = "PATH:\"" + path + "\"";

        List<NodeRef> results = this.searchForNodeRefs(query);
        if (results.size() > 0) {
            ref = results.get(0);
        }

        return ref;
    }
    
    /* (non-Javadoc)
     * @see com.lateralminds.alfresco.util.LateralService#getContentInputStream(org.alfresco.service.cmr.repository.NodeRef)
     */
    public InputStream getContentInputStream(NodeRef nodeRef)
    {
        return getContentInputStream(nodeRef, ContentModel.PROP_CONTENT);
    }

    public InputStream getContentInputStream(NodeRef nodeRef, QName property)
    {
        ContentReader reader = contentService.getReader(nodeRef, property);
        if (reader != null) {
            return reader.getContentInputStream();
        } else {
            return null;
        }
        
    }

    /**
     * Trivial File Copy, could be useful...
     * 
     * @param src
     * @param dst
     * @throws IOException
     */
    public void copyFile(File src, File dst) throws IOException {
        if (src == null || dst == null) {
            throw new IllegalArgumentException("src and dst mustn't be NULL");
        }
        copy(new FileInputStream(src),new FileOutputStream(dst));
     }
    
    /**
     * copy streams, useful for all kinds of things
     * 
     * @param in
     * @param out
     * @throws IOException
     */
    public void copy(InputStream in, OutputStream out) throws IOException {
        if (in == null || out == null) {
            throw new IllegalArgumentException("inputStream and outputStream mustn't be NULL");
        }
        // Transfer bytes from in to out
        byte[] buf = new byte[1024];
        int len;
        try {
             while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
            }
        } finally {
            if (in != null){
                in.close();
            }
            if (out != null) {
                out.close();
             }
        }    
    }
    
    /* (non-Javadoc)
     * @see com.lateralminds.alfresco.util.LateralService#writeNodeToFile(org.alfresco.service.cmr.repository.NodeRef, java.io.File)
     */
    public void writeNodeToFile(NodeRef docRef, File outFile)
            throws IOException
    {
        if (docRef == null || outFile == null) {
            throw new IllegalArgumentException("nodeRef and outFile mustn't be NULL");
        }
        copy(getContentInputStream(docRef),new FileOutputStream(outFile));
    }

    /**
     * import a file system file into an Alfresco Space
     *
     * @param file
     * @param targetSpaceRef
     * @param nodeType defaults to cm:content if null
     * @param name defaults to file.getName() if null
     * @param title defaults to file.getName() if null
     * @return the ref to the newly created node
     * @throws IOException
     */
    public NodeRef importFile(File file, NodeRef targetSpaceRef, QName nodeType, String name, String title) throws IOException
    {
        NodeRef theFileRef = null;
        // perform some sanity checks first
        if (!file.canRead()) {
            throw new IOException("Can't open the input file for reading:");
        }
        
        String targetName = name != null ? name : file.getName();
        String targetTitle = title != null ? title : targetName;
        QName targetType = nodeType != null ? nodeType : ContentModel.TYPE_CONTENT;
        
        // check if a node with this name already exists in the directory
        if (fileFolderService.searchSimple(targetSpaceRef, targetName) != null) {
            throw new IOException("Node with the same name exists:"+targetName);
        }
        
        // now create the node within the targetSpace
        FileInfo newNodeInfo = fileFolderService.create(targetSpaceRef, targetName, targetType);
        theFileRef = newNodeInfo.getNodeRef();
        
        // set the node title
        nodeService.setProperty(theFileRef, ContentModel.PROP_TITLE, targetTitle);
        
        // set the content
        String fileMimeType = getMimetypeFor(file.getName());
        setNodeContent(theFileRef, new FileInputStream(file), fileMimeType);
        
        return theFileRef;
    }

    /**
     * convenience method for importFile() uses
     * name as title and uses cm:content as node type
     * 
     * @param file
     * @param targetSpaceRef
     * @param name
     * @return the ref to the newly created node
     * @throws IOException
     */
    public NodeRef importFile(File file, NodeRef targetSpaceRef, String name) throws IOException {
        return importFile(file, targetSpaceRef, null, name, null);
    }
    
    /**
     * convenience method for importFile() uses
     * filename as node name and title and
     * cm:content as node type
     *
     * @param file
     * @param targetSpaceRef
     * @return the ref to the newly created node
     * @throws IOException
     */
    public NodeRef importFile(File file, NodeRef targetSpaceRef) throws IOException {
        return importFile(file, targetSpaceRef, null, null, null);
    }
    
    /**
     * import a file system directory into an Alfresco Space
     * 
     * @param file
     * @param targetSpaceRef
     * @throws IOException 
     */
    public NodeRef importDirectory(File dir, NodeRef targetSpaceRef, QName nodeType, String name, String title) throws IOException
    {
        NodeRef theFileRef = null;
        // perform some sanity checks first
        if (!dir.canRead()) {
            throw new IOException("Can't open the input file for reading:");
        }
        
        String targetName = name != null ? name : dir.getName();
        String targetTitle = title != null ? title : targetName;
        QName targetType = nodeType != null ? nodeType : ContentModel.TYPE_FOLDER;
        
        // check if a node with this name already exists in the directory
        if (fileFolderService.searchSimple(targetSpaceRef, targetName) != null) {
            throw new IOException("Node with the same name exists:"+targetName);
        }
        
        // now create the node within the targetSpace
        FileInfo newNodeInfo = fileFolderService.create(targetSpaceRef, targetName, targetType);
        theFileRef = newNodeInfo.getNodeRef();
        
        // set the node title
        nodeService.setProperty(theFileRef, ContentModel.PROP_TITLE, targetTitle);
        
        File[] zipContents = dir.listFiles();

        for (File file : zipContents) {
            if (file.isDirectory()) {
                importDirectory(file, theFileRef);
            }
            if (file.isFile()) {
                importFile(file,theFileRef);
            }
        }
        return theFileRef;
    }
    
    /**
     * 
     */
    public NodeRef importDirectory(File dir, NodeRef targetSpaceRef) throws IOException
    {
        return importDirectory(dir, targetSpaceRef, null, null, null);
    }

    private VersionService versionService;

    private ImporterBootstrap bootstrap;

    private SearchService searchService;

    private NodeService nodeService;

    private ContentService contentService;

    private MimetypeService mimetypeService;
    
    private FileFolderService fileFolderService;
    
    private NamespaceService namespaceService;

    public void setFileFolderService(FileFolderService fileFolderService)
    {
        this.fileFolderService = fileFolderService;
    }

    public void setContentService(ContentService contentService)
    {
        this.contentService = contentService;
    }

    public void setMimetypeService(MimetypeService mimetypeService)
    {
        this.mimetypeService = mimetypeService;
    }

    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }

    public void setSearchService(SearchService searchService)
    {
        this.searchService = searchService;
    }

    public void setBootstrap(ImporterBootstrap bootstrap)
    {
        this.bootstrap = bootstrap;
    }

    private static Logger logger = Logger.getLogger(LateralServiceImpl.class);

    public void setVersionService(VersionService versionService)
    {
        this.versionService = versionService;
    }

    public void setNamespaceService(NamespaceService namespaceService)
    {
        this.namespaceService = namespaceService;
    }
    
    
}